/*******************************************************************************
 * This file is part of Crunch Network.
 *
 * Crunch Network is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Network is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Network.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.network;

import com.crunch.network.util.BitStreamReader;

import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * TODO describe protocol
 */
public class NetworkReader {
	/**
	 * Constructs a {@code NetworkReader} instance with a reference to the message registry and network settings.
	 *
	 * @param   messageRegistry a reference to the message registry.
	 */
	NetworkReader(MessageRegistry messageRegistry, NetworkSettings settings) {
		error = false;

		this.messageRegistry = messageRegistry;
		this.settings = settings;

		// compute ghost ID bits - we're the reader so we use max remote ghosts
		ghostIdBits = 1;
		int ghostRange = 2;
		while (ghostRange < settings.maxRemoteGhosts) {
			++ghostIdBits;
			ghostRange *= 2;
		}

		final int EXPIRATION_TIME_PADDING_MS = 2000;
		messageDependencyGraph = new MessageDependencyGraph(settings.connectionTimeoutMs + EXPIRATION_TIME_PADDING_MS) {
			@Override
			public void executeMessage(int messageId) {
				NetworkReader.this.executeGuaranteedMessage(messageId);
			}
		};

		guaranteedMessages = new HashMap<Integer, Message>();
	}

	/**
	 * Returns whether the reader has previously encountered an error.
	 *
	 * @return  whether the reader has previously encountered an error.
	 */
	public boolean hasError() {
		return error;
	}

	/**
	 * Called periodically to allow messages to expire in the message dependency graph.
	 *
	 * @param   currentTimeMs the current time in milliseconds.
	 */
	void update(long currentTimeMs) {
		if (error) {
			return;
		}

		try {
			messageDependencyGraph.expireMessages(currentTimeMs);
		} catch (NetworkException e) {
			error = true;
			settings.callbacks.log(NetworkCallbacks.LogLevel.ERROR, e.getMessage());
		}
	}

	/**
	 * Called directly after a packet has been received. Each message is read from the packet. Non-guaranteed messages
	 * are executed immediately if all their dependencies have been executed and discarded otherwise. Guaranteed
	 * messages are recursively executed as dependencies become fulfilled.
	 *
	 * @param   packet the packet that has just been received.
	 */
	void onPacketReceived(NetworkPacket packet, long currentTimeMs) {
		if (error) {
			return;
		}

		// while reading, catch underflow exceptions, as they indicate a data error
		try {
			// set up the reader for the packet
			if (packet.getPacketContentSize() < 1) {
				// we can't even read the 3-bit "unused bits" indicator - something is wrong
				throw new BufferUnderflowException();
			}

			// first, just read the 3-bit "unused bits" field
			BitStreamReader bitStreamReader = new BitStreamReader(
					ByteBuffer.wrap(packet.getPacketData()),
					packet.getPacketContentOffset() * Byte.SIZE,
					packet.getPacketContentOffset() * Byte.SIZE + 3);
			int unusedBits = (int) bitStreamReader.readUnsignedInt(3);

			// compute the actual content size by subtracting the number of unused bits
			int sizeBytes = packet.getPacketContentSize();
			int sizeBits = sizeBytes * Byte.SIZE - unusedBits;

			// reconstruct the bit stream reader to used the revised size
			bitStreamReader = new BitStreamReader(
					ByteBuffer.wrap(packet.getPacketData()),
					(packet.getPacketContentOffset() * Byte.SIZE) + 3, // start after the 3 bits we just read
					(packet.getPacketContentOffset() * Byte.SIZE) + (sizeBits - 3));

			// keep reading messages while there is space in the packet
			while (bitStreamReader.remaining() > 0) {
				// read the message network ID
				int networkId = (int) bitStreamReader.readUnsignedInt(messageRegistry.getNetworkIdBits());

				// make sure this is a valid network ID
				int messageId = messageRegistry.getMessageId(networkId);
				if (messageId < 0) {
					throw new NetworkException("Packet data error: invalid network ID " + networkId);
				}

				// read whether the message is guaranteed
				boolean guaranteed = bitStreamReader.readBoolean();
				int sequenceNumber = 0;

				if (guaranteed) {
					// read sequence number
					sequenceNumber = (int) bitStreamReader.readUnsignedInt(settings.messageSequenceNumberBits);
				}

				// keep reading dependencies until we reach a list terminator bit (0)
				ArrayList<Integer> dependencies = new ArrayList<Integer>();
				boolean end = false;
				while (!end) {
					end = bitStreamReader.readBoolean();
					if (!end) {
						int dependencyId = (int) bitStreamReader.readUnsignedInt(settings.messageSequenceNumberBits);
						dependencies.add(dependencyId);
					}
				}

				// construct and read the message
				Message message = messageRegistry.createMessage(messageId);
				StateTableMessage stMessage = null;
				if (messageRegistry.getRegisteredType(messageId) == MessageRegistry.RegisteredType.STATE_TABLE) {
					stMessage = (StateTableMessage) message;
					stMessage.init(messageRegistry.createStateTable(messageId), ghostIdBits);
				}

				message.read(bitStreamReader);
				if (stMessage != null && stMessage.getGhostId() >= settings.maxRemoteGhosts) {
					throw new NetworkException("Invalid remote ghost ID " + stMessage.getGhostId());
				}

				if (guaranteed) {
					// add to the graph and execute when no dependencies remain
					if (guaranteedMessages.containsKey(sequenceNumber) || messageDependencyGraph.hasMessageExecuted(sequenceNumber)) {
						// discard the message - it is a duplicate
						// this is rare, but possible if many packets are dropped at once (more than the ACK bitfield can hold)
						// in this case, the sender assumes packets have been dropped and resends messages in them
						messageRegistry.destroyMessage(message);
					} else {
						int[] dependencyArray = new int[dependencies.size()];
						for (int i = 0; i < dependencies.size(); ++i) {
							dependencyArray[i] = dependencies.get(i);
						}

						// add to the graph
						messageDependencyGraph.addMessage(sequenceNumber, dependencyArray, currentTimeMs);
						// try executing message - might not occur immediately if dependencies exist
						messageDependencyGraph.setMessageReadyToExecute(sequenceNumber);
					}
				} else {
					// execute immediately if all dependencies have been executed
					// otherwise, discard without executing
					boolean allDependenciesComplete = true;
					for (int i = 0; allDependenciesComplete && i < dependencies.size(); ++i) {
						if (!messageDependencyGraph.hasMessageExecuted(dependencies.get(i))) {
							allDependenciesComplete = false;
						}
					}
					if (allDependenciesComplete) {
						message.execute();
					}
					// free the message since we no longer need it
					if (stMessage != null) {
						messageRegistry.destroyStateTable(stMessage.getStateTable());
					}
					messageRegistry.destroyMessage(message);
				}
			}
		} catch (NetworkException e) {
			error = true;
			settings.callbacks.log(NetworkCallbacks.LogLevel.ERROR, e.getMessage());
		} catch (BufferUnderflowException e) {
			error = true;
			settings.callbacks.log(NetworkCallbacks.LogLevel.ERROR, "Packet data underflow error");
		} catch (IOException e) {
			error = true;
			if (e.getMessage() == null) {
				settings.callbacks.log(NetworkCallbacks.LogLevel.ERROR, "Packet data error: invalid message content");
			} else {
				settings.callbacks.log(NetworkCallbacks.LogLevel.ERROR, "Packet data error: invalid message content: " +
						e.getMessage());
			}
		}
	}

	// executes a guaranteed message and removes it from storage
	private void executeGuaranteedMessage(int sequenceNumber) {
		Message message = guaranteedMessages.remove(sequenceNumber);
		message.execute();
		// we can now free the message
		if (messageRegistry.getRegisteredType(message.getId()) == MessageRegistry.RegisteredType.STATE_TABLE) {
			StateTableMessage stMessage = (StateTableMessage) message;
			messageRegistry.destroyStateTable(stMessage.getStateTable());
		}
		messageRegistry.destroyMessage(message);
	}

	private boolean error;

	private MessageRegistry messageRegistry;
	private NetworkSettings settings;
	private int ghostIdBits;

	// graph which keeps track of dependencies between messages so that they can be executed in the proper order
	private MessageDependencyGraph messageDependencyGraph;

	// maps sequence numbers to guaranteed messages
	private HashMap<Integer, Message> guaranteedMessages;
}
